﻿#region Using Directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ZJ.Finance.DataModel
{
    /// <summary>
    /// Provides the base functionality required to parse search terms.
    /// </summary>
    public abstract class ExpressionParserBase
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the ExpressionParserBase class.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="comparisonType"></param>
        /// <param name="ignoreCase"></param>
        protected ExpressionParserBase(String propertyName, SqlComparisonType comparisonType, bool ignoreCase)
        {
            PropertyName = propertyName;
            ComparisonType = comparisonType;
            IgnoreCase = ignoreCase;
        }

        #endregion Constructors

        #region Methods

        /// <summary>
        /// Appends the specified search text to the current expression.
        /// </summary>
        /// <param name="searchText">The search text to append.</param>
        protected void ParseCore(String searchText)
        {
            IList<String> quotedValues = new List<String>();

            int leftNumber = 0;
            int rightNumber = 0;
            int i = -1;

            // last param was a key word
            bool isKeyWord = false;

            // used to track if the first param is a key word
            // i.e., and or "john smith"
            int numParams = 0;

            // use AND to search all
            // i.e. John Smith would become John and Smith
            // however "John Smith" is one entity
            bool needToInsertAND = false;

            String outStr = ParseQuotes(searchText, quotedValues);
            StringTokenizer tokenizer = new StringTokenizer(outStr, "( ),\t\r\n", true);
            String nextToken;

            while (tokenizer.HasMoreTokens)
            {
                // trim token
                nextToken = tokenizer.NextToken.Trim();

                // left parenthesis
                if (nextToken.Equals(SqlUtil.LEFT))
                {
                    leftNumber++;

                    if (needToInsertAND)
                    {
                        AppendAnd();
                    }

                    OpenGrouping();

                    needToInsertAND = false;
                    isKeyWord = false;
                }

                    // right parenthesis
                else if (nextToken.Equals(SqlUtil.RIGHT))
                {
                    rightNumber++;

                    CloseGrouping();

                    needToInsertAND = true;
                    isKeyWord = false;
                }
                    // comma
                else if (nextToken.Equals(SqlUtil.COMMA))
                {
                    AppendOr();
                    needToInsertAND = false;
                    isKeyWord = false;
                }
                    // token is a key word (such as AND, OR,...)
                else if (IsKeyWord(nextToken))
                {
                    numParams++;

                    // if this is the first parameter in the entire string
                    // treat it as a regular param (not a key word)
                    if (numParams == 1)
                    {
                        needToInsertAND = true;

                        AppendSearchText(nextToken);
                    }

                        // if only two params
                    else if ((numParams == 2) && (tokenizer.CountTokens <= 1))
                    {
                        AppendAnd();
                        AppendSearchText(nextToken);
                    }

                        // if the last string was a key word
                    else if (isKeyWord)
                    {
                        needToInsertAND = true;

                        // treat this param as a regular string, not a key word
                        isKeyWord = false;

                        AppendSearchText(nextToken);
                    }
                    else
                    {
                        // make sure this is not the last param, if so use AND to search all
                        if (tokenizer.CountTokens <= 1)
                        {
                            AppendAnd();
                            AppendSearchText(nextToken);
                        }
                        else
                        {
                            if (SqlUtil.AND.Equals(nextToken, StringComparison.OrdinalIgnoreCase))
                            {
                                AppendAnd();
                            }
                            else if (SqlUtil.OR.Equals(nextToken, StringComparison.OrdinalIgnoreCase))
                            {
                                AppendOr();
                            }

                            needToInsertAND = false;
                            isKeyWord = true;
                        }
                    }
                }
                else if (nextToken.Equals(" "))
                {
                    AppendSpace();
                }
                else if (nextToken.Equals(""))
                {
                }
                    // string in quotes
                else if (nextToken.Equals(SqlUtil.TOKEN))
                {
                    numParams++;

                    if (needToInsertAND)
                    {
                        AppendAnd();
                    }

                    needToInsertAND = true;

                    // if the search param string is like: "John Smith" and Jones
                    // the and needs to be translated to a SQL "AND"
                    isKeyWord = false;

                    // get the next quoted string
                    i++;

                    AppendSearchText(quotedValues[i]);
                }
                    // a regular string other than the above cases
                else
                {
                    numParams++;

                    if (needToInsertAND)
                    {
                        AppendAnd();
                    }

                    needToInsertAND = true;
                    isKeyWord = false;

                    AppendSearchText(nextToken);
                }
            }

            if (leftNumber != rightNumber)
            {
                throw new ArgumentException("Syntax Error: mismatched parenthesis.");
            }
        }

        /// <summary>
        /// Parses quoted search terms.
        /// </summary>
        /// <param name="searchText"></param>
        /// <param name="quotedValues"></param>
        /// <returns></returns>
        private String ParseQuotes(String searchText, IList<String> quotedValues)
        {
            // sanity check
            if (String.IsNullOrEmpty(searchText) || searchText.IndexOf('"') < 0)
            {
                return searchText;
            }

            String[] tokens = searchText.Split('"');
            StringBuilder sb = new StringBuilder();
            bool needEndQuotes = true;

            foreach (String token in tokens)
            {
                needEndQuotes = !needEndQuotes;

                if (needEndQuotes)
                {
                    sb.Append(SqlUtil.TOKEN);
                    quotedValues.Add(token);
                }
                else
                {
                    sb.Append(token);
                }
            }

            if (needEndQuotes)
            {
                throw new ArgumentException("Syntax Error: mismatched quotes.");
            }

            return sb.ToString();
        }

        /// <summary>
        /// Determines whether the specified word is a reserved keyword.
        /// </summary>
        /// <param name="word"></param>
        /// <returns></returns>
        private bool IsKeyWord(String word)
        {
            return (word != null && (
                                        SqlUtil.AND.Equals(word, StringComparison.OrdinalIgnoreCase) ||
                                        SqlUtil.OR.Equals(word, StringComparison.OrdinalIgnoreCase)
                                    ));
        }

        #endregion Methods

        #region Abstract Methods

        /// <summary>
        /// Appends an OR expression.
        /// </summary>
        protected abstract void AppendOr();

        /// <summary>
        /// Appends an AND expression.
        /// </summary>
        protected abstract void AppendAnd();

        /// <summary>
        /// Appends an expression separator.
        /// </summary>
        protected abstract void AppendSpace();

        /// <summary>
        /// Appends a group opened expression.
        /// </summary>
        protected abstract void OpenGrouping();

        /// <summary>
        /// Appends a group closed expression.
        /// </summary>
        protected abstract void CloseGrouping();

        /// <summary>
        /// Appends the specified search text to the expression.
        /// </summary>
        /// <param name="searchText">The search text to append.</param>
        protected abstract void AppendSearchText(String searchText);

        #endregion Abstract Methods

        #region Properties

        /// <summary>
        /// The ComparisonType member variable.
        /// </summary>
        private SqlComparisonType comparisonType;

        /// <summary>
        /// The IgnoreCase member variable.
        /// </summary>
        private bool ignoreCase;

        /// <summary>
        /// The PropertyName member variable.
        /// </summary>
        private String propertyName;

        /// <summary>
        /// Gets or sets the IgnoreCase property.
        /// </summary>
        public bool IgnoreCase
        {
            get { return ignoreCase; }
            set { ignoreCase = value; }
        }

        /// <summary>
        /// Gets or sets the PropertyName property.
        /// </summary>
        public String PropertyName
        {
            get { return propertyName; }
            set { propertyName = value; }
        }

        /// <summary>
        /// Gets or sets the ComparisonType property.
        /// </summary>
        public SqlComparisonType ComparisonType
        {
            get { return comparisonType; }
            set { comparisonType = value; }
        }

        #endregion Properties
    }
}